由于在X86平台上,内存是划分为低端内存和高端内存的,所以在这两个区域内的page查找对应的虚拟地址是不一样的。

page_address()的定义

include/linux/mm.h中,有对page_address()函数的三种定义,主要依赖于不同的平台:

首先看几个宏的定义:

  • CONFIG_HIGHMEM

    是否支持高端内存,可以查看config文件,一般推荐内存超过896M的时候,才配置为支持高端内存。

  • WANT_PAGE_VIRTUAL

    X86平台没有定义。所以下面的HASHED_PAGE_VIRTUAL在支持高端内存的i386平台上是有定义的:

    1
    2
    3
    #if defined(CONFIG_HIGHMEM) && !defined(WANT_PAGE_VIRTUAL)
    #define HASHED_PAGE_VIRTUAL
    #endif

所以下面page_address()的定义在i386上是没有定义的:

1
2
3
4
5
6
7
8
#if defined(WANT_PAGE_VIRTUAL)
#define page_address(page) ((page)->virtual)
#define set_page_address(page, address) \\
do { \\
(page)->virtual = (address); \\
} while(0)
#define page_address_init() do { } while(0)
#endif

在没有配置CONFIG_HIGHMEM的i386平台上,page_address是这样定义的:

1
2
3
4
5
#if !defined(HASHED_PAGE_VIRTUAL) && !defined(WANT_PAGE_VIRTUAL)
#define page_address(page) lowmem_page_address(page)
#define set_page_address(page, address) do { } while(0)
#define page_address_init() do { } while(0)
#endif

而在支持高端内存的i386平台上,page_address()定义如下:

1
2
3
4
5
#if defined(HASHED_PAGE_VIRTUAL)
void *page_address(struct page *page);
void set_page_address(struct page *page, void *virtual);
void page_address_init(void);
#endif

page_address()在低端内存中的实现

函数实现如下:

1
2
3
4
5
6
7
#define page_address(page) lowmem_page_address(page)
static __always_inline void *lowmem_page_address(const struct page *page)
{
return page_to_virt(page);
}
#define page_to_virt(x) __va(PFN_PHYS(page_to_pfn(x)))
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))

小于896M的物理地址空间和3G~3G+896M的内核地址空间是一一对应映射的,所以只要知道page对应的物理地址就可以知道这个page对应的线性地址空间(pa + PAGE_OFFSET)。在低端内存中,通过页page(struct page* page)取得虚拟地址就是这样转换的。

page_address()在高端内存中的实现

在有配置CONFIG_HIGHMEM的i386平台上,page_address的实现在mm/highmem.c中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void *page_address(const struct page *page)
{
unsigned long flags;
void *ret;
struct page_address_slot *pas;
if (!PageHighMem(page)) // 判断是否属于高端内存,若不是,则通过上面的方法找到
return lowmem_page_address(page);
pas = page_slot(page);
ret = NULL;
spin_lock_irqsave(&pas->lock, flags);
if (!list_empty(&pas->lh)) {
struct page_address_map *pam;
list_for_each_entry(pam, &pas->lh, list) {
if (pam->page == page) {
ret = pam->virtual;
goto done;
}
}
}
done:
spin_unlock_irqrestore(&pas->lock, flags);
return ret;
}

在高端内存中,由于不能通过像在低端内存中一样,直接通过物理地址加PAGE_OFFSET得到线性地址,所以引入了一个叫做page_address_map的结构,该结构保存每个page(仅高端内存中的)和对应的虚拟地址,所有高端内存中的这种映射通过链表链接起来,这个结构是在高端内存映射的时候建立,并加入到链表中的。

1
2
3
4
5
struct page_address_map {
struct page *page; // page
void *virtual; // 虚拟地址
struct list_head list; // 指向下一个该结构
};

又因为如果内存远远大于896M,那么高端内存中的page就比较多,如果只用一个链表来表示,那么查找起来就比较耗时,所以这里引入了HASH算法,采用多个链表,每个page通过一定的HASH算法,对应到一个链表上,总共有128个链表:

1
2
3
4
static struct page_address_slot {
struct list_head lh; /* List of page_address_maps */
spinlock_t lock; /* Protect this bucket's list */
} ____cacheline_aligned_in_smp page_address_htable[1<<PA_HASH_ORDER];

PA_HASH_ORDER=7,所有一共有1<<7(128)个链表,每个page通过HASH算法后对应一个page_address_htable链表,然后再遍历这个链表来找到对应的PAGE和虚拟地址。

说明

本文转自page_address()函数分析